package es.ise.core.mbg.plugins;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.Field;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.JavaVisibility;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.Parameter;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import org.mybatis.generator.api.dom.xml.Attribute;
import org.mybatis.generator.api.dom.xml.TextElement;
import org.mybatis.generator.api.dom.xml.XmlElement;
import org.mybatis.generator.internal.util.JavaBeansUtil;

import es.ise.core.mbg.plugins.utils.TableUtilities;

/**
 * Este plugin se encarga de añadir los campos de auditoría a los modelos que lo
 * requieran. En concreto, los modelos que tendrán campos de auditorías serán los
 * que posean un prefijo en la base de datos y tengan la cuaterna de columnas de
 * fecha y usurio de creación y actualización.
 * 
 * A la hora de insertar y actualizar, solo una de las parejas de campos de
 * auditoría se tendrá en cuenta: la de creación en caso de los inserts, y la
 * de actualización en caso de los updates.
 * 
 * @author Paco
 * 
 */
public class AuthoringPlugin extends PluginAdapter
{
	private static final String AUTHORING_PROPERTY = "authoring";
	private static final FullyQualifiedJavaType tipoAuditoria = new FullyQualifiedJavaType("es.ise.core.model.Auditoria");

	private enum AuthoringType
	{
		CREACION("CREACION"), ACTUALIZACION("ACTU");
		String bd;
		private AuthoringType(String bd)
		{
			this.bd = bd;
		}
		public String getBd()
		{
			return bd;
		}
	}
	
	private enum DataType
	{
		FECHA("F"), USUARIO("U");
		String bd;
		private DataType(String desc)
		{
			this.bd = desc;
		}
		public String getBd()
		{
			return bd;
		}
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * @return siempre devuelve <code>true</code> porque no hay comprobaciones que hacer
	 */
	@Override
	public boolean validate(List<String> arg0)
	{
		return true;
	}

	/**
	 * Calcula cuales serán los nombres de los campos de auditoría, y, si la tabla contiene
	 * las 4 columnas, la marca como una tabla con auditoría, y retira las 4 columnas de
	 * auditoría del modelo.
	 */
	@Override
	public void initialized(IntrospectedTable table)
	{
		// calculo los nombres
		Set<String> columns = TableUtilities.getAuthoringColumns(table);
		
		// cuento cuantas columnas de auditoría tiene la tabla
		int count = 0;
		for (IntrospectedColumn column : table.getAllColumns())
		{
			if (columns.contains(column.getActualColumnName()))
				count++;
		}
		
		// si tiene las cuatro...
		if (count == 4)
		{
			// marco la tabla como tabla con auditorías
			table.getTableConfiguration().addProperty(AUTHORING_PROPERTY, "true");
			for(String column: columns)
			{
				// y retiro cada de esas columnas del modelo
				IntrospectedColumn introspectedColumn = table.getColumn(column);
				table.getBaseColumns().remove(introspectedColumn);
			}
		}
	}

	private Field createPrivateField(AuthoringType type)
	{
		Field field = new Field(JavaBeansUtil.getCamelCaseString(type.toString(), false), tipoAuditoria);
		field.setVisibility(JavaVisibility.PRIVATE);
		return field;
	}
	
	private void addAuthoringMethods(TopLevelClass clazz, AuthoringType type)
	{
		Method getter = new Method(JavaBeansUtil.getCamelCaseString(String.format("get_%s", type), false));
		getter.setVisibility(JavaVisibility.PUBLIC);
		getter.setReturnType(tipoAuditoria);
		getter.addBodyLine(String.format("return %s;", type.toString().toLowerCase()));
		clazz.addMethod(getter);
		
		Method setter = new Method(JavaBeansUtil.getCamelCaseString(String.format("set_%s", type), false));
		setter.setVisibility(JavaVisibility.PUBLIC);
		setter.setReturnType(new FullyQualifiedJavaType("void"));
		setter.addParameter(new Parameter(tipoAuditoria, type.toString().toLowerCase()));
		setter.addBodyLine(String.format("this.%1$s = %1$s;", type.toString().toLowerCase()));
		clazz.addMethod(setter);
	}
	
	/**
	 * Crea los campos de tipo {@see es.ise.core.model.Auditoria} de creación y
	 * actualización.
	 */
	@Override
	public boolean modelBaseRecordClassGenerated(TopLevelClass clazz, IntrospectedTable table)
	{
		if(table.getTableConfigurationProperty(AUTHORING_PROPERTY) != null)
		{
			clazz.addImportedType(tipoAuditoria);
			clazz.addField(createPrivateField(AuthoringType.CREACION));
			clazz.addField(createPrivateField(AuthoringType.ACTUALIZACION));
			addAuthoringMethods(clazz, AuthoringType.CREACION);
			addAuthoringMethods(clazz, AuthoringType.ACTUALIZACION);
		}
		return true;
	}
	
	public XmlElement createResultElement(String prefix, DataType data, AuthoringType type)
	{
		XmlElement element = new XmlElement("result");
		element.addAttribute(new Attribute("column", String.format("%s_%s_%s", prefix, data.getBd(), type.getBd())));
		element.addAttribute(new Attribute("property", String.format("%s.%s", type.toString().toLowerCase(), data == DataType.FECHA ? "fecha" : "usuario.identificador")));
		element.addAttribute(new Attribute("jdbcType", data == DataType.FECHA ? "TIMESTAMP" : "VARCHAR"));
		return element;
	}

	/**
	 * Añade los campos de auditoría al resultMap del XML Mapper, para que trabaje
	 * correctamente con objetos de la clase {@see es.ise.core.model.Auditoria}
	 */
	@Override
	public boolean sqlMapResultMapWithoutBLOBsElementGenerated(XmlElement element, IntrospectedTable table)
	{
		if(table.getTableConfigurationProperty(AUTHORING_PROPERTY) != null)
		{
			String prefix = TableUtilities.getTablePrefix(table);
			element.addElement(createResultElement(prefix, DataType.FECHA, AuthoringType.CREACION));
			element.addElement(createResultElement(prefix, DataType.USUARIO, AuthoringType.CREACION));
			element.addElement(createResultElement(prefix, DataType.FECHA, AuthoringType.ACTUALIZACION));
			element.addElement(createResultElement(prefix, DataType.USUARIO, AuthoringType.ACTUALIZACION));
		}
		return true;
	}

	/**
	 * Añade los cuatro nombres de columnas de auditoría a la lista de columnas base
	 */
	@Override
	public boolean sqlMapBaseColumnListElementGenerated(XmlElement element, IntrospectedTable table)
	{
		if(table.getTableConfigurationProperty(AUTHORING_PROPERTY) != null)
		{
			element.addElement(new TextElement(", "+StringUtils.join(TableUtilities.getAuthoringColumns(table), ", ")));
		}
		return true;
	}

	@Override
	public boolean sqlMapInsertElementGenerated(XmlElement element, IntrospectedTable table)
	{
		if(table.getTableConfigurationProperty(AUTHORING_PROPERTY) != null)
		{
			String prefix = TableUtilities.getTablePrefix(table);
			int fromIndex = element.getElements().get(5) instanceof XmlElement ? 6 : 5;
			while(element.getElements().size() > fromIndex)
				element.getElements().remove(fromIndex);
			element.addElement(new TextElement(String.format("insert into %s", table.getTableConfiguration().getTableName())));
			List<String> columns = new ArrayList<String>();
			for(IntrospectedColumn column: table.getAllColumns())
			{
				columns.add(column.getActualColumnName());
			}
			columns.add(String.format("%s_F_CREACION", prefix));
			columns.add(String.format("%s_U_CREACION", prefix));
			element.addElement(new TextElement(String.format("\t(%s)", StringUtils.join(columns, ", "))));
			element.addElement(new TextElement("values"));
			List<String> values = new ArrayList<String>();
			for(IntrospectedColumn column: table.getAllColumns())
			{
				values.add(String.format("#{%s,jdbcType=%s}", column.getJavaProperty(), column.getJdbcTypeName()));
			}
			values.add("#{creacion.fecha,jdbcType=TIMESTAMP}");
			values.add("#{creacion.usuario.identificador,jdbcType=TIMESTAMP}");
			element.addElement(new TextElement(String.format("\t(%s)", StringUtils.join(values, ", "))));
		}
		return true;
	}
	
	private XmlElement createIfElement(String value, DataType data, AuthoringType type)
	{
		XmlElement element = new XmlElement("if");
		if(data == DataType.FECHA)
			element.addAttribute(new Attribute("test", String.format("%1$s != null and %1$s.fecha != null", type.toString().toLowerCase())));
		else
			element.addAttribute(new Attribute("test", String.format("%1$s != null and %1$s.usuario != null and %1$s.usuario.identificador != null", type.toString().toLowerCase())));
		element.addElement(new TextElement(String.format("\t%s", value)));
		return element;
	}

	@Override
	public boolean sqlMapInsertSelectiveElementGenerated(XmlElement element, IntrospectedTable table)
	{
		if(table.getTableConfigurationProperty(AUTHORING_PROPERTY) != null)
		{
			String prefix = TableUtilities.getTablePrefix(table);
			int offset = element.getElements().get(5) instanceof XmlElement ? 1 : 0;
			XmlElement trimColumns = (XmlElement)element.getElements().get(6+offset);
			trimColumns.addElement(createIfElement(String.format("%s_F_CREACION,", prefix), DataType.FECHA, AuthoringType.CREACION));
			trimColumns.addElement(createIfElement(String.format("%s_U_CREACION,", prefix), DataType.USUARIO, AuthoringType.CREACION));
			XmlElement trimValues = (XmlElement)element.getElements().get(7+offset);
			trimValues.addElement(createIfElement("#{creacion.fecha,jdbcType=TIMESTAMP},", DataType.FECHA, AuthoringType.CREACION));
			trimValues.addElement(createIfElement("#{creacion.usuario.identificador,jdbcType=VARCHAR},", DataType.USUARIO, AuthoringType.CREACION));
		}
		return true;
	}

	private XmlElement createRecordIfElement(String value, DataType data, AuthoringType type)
	{
		XmlElement element = new XmlElement("if");
		if(data == DataType.FECHA)
			element.addAttribute(new Attribute("test", String.format("record.%1$s != null and record.%1$s.fecha != null", type.toString().toLowerCase())));
		else
			element.addAttribute(new Attribute("test", String.format("record.%1$s != null and record.%1$s.usuario != null and record.%1$s.usuario.identificador != null", type.toString().toLowerCase())));
		element.addElement(new TextElement(String.format("\t%s", value)));
		return element;
	}
	
	@Override
	public boolean sqlMapUpdateByExampleSelectiveElementGenerated(XmlElement element, IntrospectedTable table)
	{
		if(table.getTableConfigurationProperty(AUTHORING_PROPERTY) != null)
		{
			String prefix = TableUtilities.getTablePrefix(table);
			XmlElement set = (XmlElement)element.getElements().get(6);
			set.addElement(createRecordIfElement(String.format("%s_F_ACTU = #{record.actualizacion.fecha,jdbcType=TIMESTAMP},", prefix), DataType.FECHA, AuthoringType.ACTUALIZACION));
			set.addElement(createRecordIfElement(String.format("%s_U_ACTU = #{record.actualizacion.usuario.identificador,jdbcType=VARCHAR},", prefix), DataType.USUARIO, AuthoringType.ACTUALIZACION));
		}
		return true;
	}

	@Override
	public boolean sqlMapUpdateByExampleWithoutBLOBsElementGenerated(XmlElement element, IntrospectedTable table)
	{
		if(table.getTableConfigurationProperty(AUTHORING_PROPERTY) != null)
		{
			String prefix = TableUtilities.getTablePrefix(table);
			int pos = element.getElements().size()-1;
			element.addElement(pos++, new TextElement(String.format("  , %s_F_ACTU = #{record.actualizacion.fecha,jdbcType=TIMESTAMP}", prefix)));
			element.addElement(pos++, new TextElement(String.format("  , %s_U_ACTU = #{record.actualizacion.usuario.identificador,jdbcType=VARCHAR}", prefix)));
		}
		return true;
	}

	@Override
	public boolean sqlMapUpdateByPrimaryKeySelectiveElementGenerated(XmlElement element, IntrospectedTable table)
	{
		if(table.getTableConfigurationProperty(AUTHORING_PROPERTY) != null)
		{
			String prefix = TableUtilities.getTablePrefix(table);
			XmlElement set = (XmlElement)element.getElements().get(6);
			set.addElement(createIfElement(String.format("%s_F_ACTU = #{actualizacion.fecha,jdbcType=TIMESTAMP},", prefix), DataType.FECHA, AuthoringType.ACTUALIZACION));
			set.addElement(createIfElement(String.format("%s_U_ACTU = #{actualizacion.usuario.identificador,jdbcType=VARCHAR},", prefix), DataType.USUARIO, AuthoringType.ACTUALIZACION));
		}
		return true;
	}

	@Override
	public boolean sqlMapUpdateByPrimaryKeyWithoutBLOBsElementGenerated(XmlElement element, IntrospectedTable table)
	{
		if(table.getTableConfigurationProperty(AUTHORING_PROPERTY) != null)
		{
			String prefix = TableUtilities.getTablePrefix(table);
			int pos = element.getElements().size()-1;
			element.addElement(pos++, new TextElement(String.format("  , %s_F_ACTU = #{actualizacion.fecha,jdbcType=TIMESTAMP}", prefix)));
			element.addElement(pos++, new TextElement(String.format("  , %s_U_ACTU = #{actualizacion.usuario.identificador,jdbcType=VARCHAR}", prefix)));
		}
		return true;
	}

	
}
